/*
    Copyright 2009 Barnet Wagman

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

        http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.
 */

package ot;

import ot.fs.*;
import ot.s3.*;
import java.io.*;
import org.jets3t.apps.synchronize.*;
import org.jets3t.service.*;
import org.jets3t.service.impl.rest.httpclient.*;
import java.util.*;

/** <tt>Sync</tt> contains static methods for synchronizing objects
 *  in an {@link ContextS3} with those in a {@link ContextFs} and
 *  vice versa.
 *
 * @author Barnet Wagman
 */
public class Sync {

    public OtFs otFs;
    public OtS3 otS3;

    private Jets3tProperties jp;
    private S3Service s3;

    public Sync() {}

    public void setOtFs(OtFs otFs) { this.otFs = otFs; }
    public void setOtS3(OtS3 otS3)
        throws S3ServiceException {
            this.otS3 = otS3;
            jp = otS3.mops.ops.createDefaultJets3tProperties();
            s3 = new RestS3Service(otS3.opsS3.getCredentials(), null,null,jp);
    }

    public static Sync createSyncOtFs(File fsContextDir,
                                      String s3ContextBucketName,
                                      String awsAccessID, String awsSecretKey)
        throws OtException, S3ServiceException, NoSuchContextException {
        Sync s = new Sync();
        s.setOtFs(new OtFs(new ContextFs(fsContextDir),// ));
                                         new GzipSerializer()));
        s.setOtS3(new OtS3(new ContextS3(s3ContextBucketName),
                           awsAccessID, awsSecretKey, //);
                           new GzipSerializer()));
        return(s);
    }

        /** Works recursively to sync everything in the tree
         *  starting with <tt>sourceDir</tt> as its root.  To
         *  sync the entire context, set <tt>sourceDir</tt> to null,
         *
         * @param receivingContext
         * @param sourceContext
         * @param sourceDir if null, the entire context is sync'd.
         */
    public void sync(Context receivingContext,Context sourceContext,
                     DPath sourceDir) throws Exception {

        if ( (receivingContext instanceof ContextS3) &&
             (sourceContext instanceof ContextFs) ) {
            syncToS3FromFs((ContextS3) receivingContext,
                           (ContextFs) sourceContext,sourceDir);
        }
        else if ( (receivingContext instanceof ContextFs) &&
                  (sourceContext instanceof ContextS3) ) {
            syncToFsFromS3((ContextFs) receivingContext,
                           (ContextS3) sourceContext, sourceDir);
        }
        else throw new UnsupportedOperationException(
            "sync of " + sourceContext.getClass().getName() + " -> " +
            receivingContext.getClass().getName() + " is not supported yet.");
    }

        // -------------------------------------------------

    void syncToS3FromFs(ContextS3 receivingContext,
                        ContextFs sourceContext,DPath sourceDir)
        throws Exception {

        File sourceDirFile;
        if ( sourceDir == null )
            sourceDirFile = sourceContext.dir;
        else
            sourceDirFile = new File(sourceContext.dir,
                                     sourceDir.toPathString());

        List fileList = getFiles(sourceDirFile);
        List<FilesInDir> fids = groupFilesByDir(fileList);       

        for ( FilesInDir fid : fids ) {           
            writeLocalFilesToS3(
                fid.getS3Path(receivingContext.bucketName,sourceContext),
                fid.files);           
        }
    }

    void syncToFsFromS3(ContextFs receivingContext,
                        ContextS3 sourceContext, DPath sourceDir)
        throws Exception {

        String s3Path;
        File localReceivingDir;
        if ( sourceDir != null ) {
            s3Path = sourceContext.bucketName + "/" +
                    sourceDir.toPathString();
            localReceivingDir =
                new File(receivingContext.dir,sourceDir.toPathString());
        }
        else {
            s3Path = sourceContext.bucketName;
            localReceivingDir = receivingContext.dir;
        }

        writeS3ObjectsToLocalFiles(s3Path,localReceivingDir);
    }

        // -------------------------------------------------

   private List<File> getFiles(File dir) {

        List<File> lst = new ArrayList<File>();

        File[] fs = dir.listFiles();
        for ( File f : fs ) {
            if ( f.isDirectory() ) {
                lst.addAll(getFiles(f));
            }
            else lst.add(f);
        }
        return(lst);
    }
   
    class FilesInDir {
        File dir;
        List files;

        FilesInDir(File dir) {
            this.dir = dir;
            files = new ArrayList();
        }

        String getS3Path(String bucketName,ContextFs context)
            throws OtException {

                // Make sure that dir is in the context
            if ( !dir.getPath().startsWith(context.dir.getPath()) )
                throw new OtException("Local dir " + dir.getPath() +
                    " is NOT in context=" + context.toString());

                // Strip off the context path
            String relPath =
                dir.getPath().substring(context.dir.getPath().length() + 1);

            return( bucketName + "/" + relPath);
        }
    }

    List<FilesInDir> groupFilesByDir(List<File> files) {

        HashMap<String,FilesInDir> ht = new HashMap<String,FilesInDir>();
            // ^ keyed by dir path string

        for ( File f : files ) {
            String k = f.getParentFile().getPath();
            FilesInDir fid = ht.get(k);
            if ( fid == null ) {
                fid = new FilesInDir(f.getParentFile());
                ht.put(k,fid);
            }
            fid.files.add(f);
        }

        List<FilesInDir> fids = new ArrayList<FilesInDir>();
        for ( Iterator<FilesInDir> it = ht.values().iterator();
              it.hasNext(); ) {
            fids.add(it.next());
        }

        int n = 0;
        for ( FilesInDir fid : fids ) {
            n += fid.files.size();
        }
        if ( n != files.size() ) throw new RuntimeException(
                    " Grouping ERROR: " + n + " != " + files.size());

        return(fids);
    }

    void writeLocalFilesToS3(String s3Path,List fileList) throws Exception {

            // Options
        boolean doAction = true;
        boolean isQuiet = false;
        boolean isNoProgress = false;
        boolean isForce = false;
        boolean isKeepFiles = false;
        boolean isNoDelete = true; // false;
        boolean isGzipEnabled = false;
        boolean isEncryptionEnabled = false;
        boolean isMoveEnabled = false;
        boolean isBatchMode = false;
        boolean isSkipMetadata = false;
        String aclString = "PRIVATE";
        int reportLevel = 3; // Synchronize.REPORT_LEVEL_ALL;

       Synchronize s =
            new Synchronize(s3,
                            doAction, isQuiet, isNoProgress,
                            isForce, isKeepFiles, isNoDelete,
                            isMoveEnabled, isBatchMode, isSkipMetadata,
                            isGzipEnabled, isEncryptionEnabled,
                            reportLevel, jp);
      
       s.run(s3Path,fileList,"UP",null,aclString);       
    }

    void writeS3ObjectsToLocalFiles(String s3Path,
                                    File localReceivingDir)
            throws Exception {

            // Options
        boolean doAction = true;
        boolean isQuiet = false;
        boolean isNoProgress = false;
        boolean isForce = false;
        boolean isKeepFiles = false;
        boolean isNoDelete = true; // false;
        boolean isGzipEnabled = false;
        boolean isEncryptionEnabled = false;
        boolean isMoveEnabled = false;
        boolean isBatchMode = false;
        boolean isSkipMetadata = false;
        String aclString = "PRIVATE";
        int reportLevel = 3; // Synchronize.REPORT_LEVEL_ALL;

       Synchronize s =
            new Synchronize(s3,
                            doAction, isQuiet, isNoProgress,
                            isForce, isKeepFiles, isNoDelete,
                            isMoveEnabled, isBatchMode, isSkipMetadata,
                            isGzipEnabled, isEncryptionEnabled,
                            reportLevel, jp);
      
       List fList = new ArrayList();
       fList.add(localReceivingDir);
       s.run(s3Path,fList,"DOWN",null,aclString);
    }
}
